﻿//-----------------------------------------------------------------------
// <copyright file="MembershipRepository.cs" company="OOHM PROCESSAMENTO DE DADOS LTDA.">
//     Microsoft Public License (MS-PL) This license governs use of the accompanying
//     software. If you use the software, you accept this license. If you do not
//     accept the license, do not use the software. 1. Definitions The terms "reproduce,"
//     "reproduction," "derivative works," and "distribution" have the same meaning
//     here as under U.S. copyright law. A "contribution" is the original software,
//     or any additions or changes to the software. A "contributor" is any person
//     that distributes its contribution under this license. "Licensed patents"
//     are a contributor's patent claims that read directly on its contribution.
//     2. Grant of Rights (A) Copyright Grant- Subject to the terms of this license,
//     including the license conditions and limitations in section 3, each contributor
//     grants you a non-exclusive, worldwide, royalty-free copyright license to
//     reproduce its contribution, prepare derivative works of its contribution,
//     and distribute its contribution or any derivative works that you create.
//     (B) Patent Grant- Subject to the terms of this license, including the license
//     conditions and limitations in section 3, each contributor grants you a non-exclusive,
//     worldwide, royalty-free license under its licensed patents to make, have
//     made, use, sell, offer for sale, import, and/or otherwise dispose of its
//     contribution in the software or derivative works of the contribution in
//     the software. 3. Conditions and Limitations (A) No Trademark License- This
//     license does not grant you rights to use any contributors' name, logo, or
//     trademarks. (B) If you bring a patent claim against any contributor over
//     patents that you claim are infringed by the software, your patent license
//     from such contributor to the software ends automatically. (C) If you distribute
//     any portion of the software, you must retain all copyright, patent, trademark,
//     and attribution notices that are present in the software. (D) If you distribute
//     any portion of the software in source code form, you may do so only under
//     this license by including a complete copy of this license with your distribution.
//     If you distribute any portion of the software in compiled or object code
//     form, you may only do so under a license that complies with this license.
//     (E) The software is licensed "as-is." You bear the risk of using it. The
//     contributors give no express warranties, guarantees or conditions. You may
//     have additional consumer rights under your local laws which this license
//     cannot change. To the extent permitted under your local laws, the contributors
//     exclude the implied warranties of merchantability, fitness for a particular
// </copyright>
//-----------------------------------------------------------------------
namespace AbstractSaaSTemplate.Infrastructure.Mocks.Repositories{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Text;
    using AbstractSaaSTemplate.Domain.Models.Entities;
    using AbstractSaaSTemplate.Domain.Models.Enumerators;
    using AbstractSaaSTemplate.Domain.Models.ValueObjects;
    using AbstractSaaSTemplate.Domain.Repositories;
    using AbstractSaaSTemplate.Infrastructure.Mocks.Repositories.Core;
    using OOHM.Framework.Configuration;
    using OOHM.Framework.Security.Cryptography;

    /// <summary>
    /// Implements 'IMembershipRepository' and all its contract, providing
    /// data accessibility to the aggregate 'Membership'.
    /// </summary>
    public sealed class MembershipRepository : IMembershipRepository
    {
        /// <summary>
        /// Initializes a new instance of the MembershipRepository class
        /// with some default data used #if DEBUG.
        /// </summary>
        public MembershipRepository()
        {
#if DEBUG
            // DEFAULT DATA LOADING RELATED TO 'INSTALLATION'
            const string StaffAdminEmailAddress = "admin@site.com";
            const string StaffAdminPassword = "123456";

            if (Configurator.Initialized)
            {

                var staffAdminUser = UsersTable
                    .Where(d => d.EmailAddress == StaffAdminEmailAddress)
                    .FirstOrDefault();

                if (staffAdminUser == null)
                {
                    staffAdminUser = new User()
                    {
                        AccessLevel = AccessLevels.Distributor,
                        EmailAddress = StaffAdminEmailAddress,
                        Credentials = new Credentials(StaffAdminEmailAddress, StaffAdminPassword),
                        Culture = CultureInfo.GetCultureInfo("pt-BR")
                    };

                    this.AddOrUpdateUser(staffAdminUser);

                    // plus generates an association between the member and the role.
                    this.AssociateUserWithRole(
                        staffAdminUser.Id,
                        AccessRoles.Administrator);

                    // and we add the access to feature users management,
                    // tenants management and to the security management
                    var features = new[]
                    {
                        ControlledFeatures.Dashboard,
                        ControlledFeatures.TenantsManagement,
                        ControlledFeatures.UsersManagement,
                        ControlledFeatures.RolesFeaturesManagement
                    };

                    foreach (var feature in features)
                    {
                        this.AssociateRoleWithFeature(
                            AccessLevels.Distributor,
                            AccessRoles.Administrator,
                            feature);
                    }
                }

                // DEFAULT DATA LOADING RELATED TO 'CUSTOMISATION'.
                if (UserTenantsTable.Count < 2)
                {
                    var tenant1Admin = new User()
                    {
                        Id = 2,
                        FirstName = "Mr",
                        LastName = "Alpha",
                        AccessLevel = AccessLevels.Tenant,
                        EmailAddress = "admin@tenant-alpha.com",
                        Credentials = new Credentials(
                            "admin@tenant-alpha.com",
                            "123456")
                    };
                    var tenant1AdminAssociation = new UserTenant() { UserId = tenant1Admin.Id, TenantId = 1 };
                    UsersTable.Add(tenant1Admin);
                    UserTenantsTable.Add(tenant1AdminAssociation);

                    var tenant2Admin = new User()
                    {
                        Id = 3,
                        FirstName = "Mr",
                        LastName = "Beta",
                        AccessLevel = AccessLevels.Tenant,
                        EmailAddress = "admin@tenant-beta.com",
                        Credentials = new Credentials(
                            "admin@tenant-beta.com",
                            "123456")
                    };
                    var tenant2AdminAssociation = new UserTenant() { UserId = tenant2Admin.Id, TenantId = 2 };
                    UsersTable.Add(tenant2Admin);
                    UserTenantsTable.Add(tenant2AdminAssociation);
                }
            }
#endif
        }

        /// <summary>
        /// Gets the table of 'Account'
        /// </summary>
        private static IList<Account> AccountsTable
        {
            get
            {
                return InMemoryRepositoryHelper.GetTable<Account>();
            }
        }
        
        /// <summary>
        /// Gets the table of 'User'
        /// </summary>
        private static IList<User> UsersTable
        {
            get
            {
                return InMemoryRepositoryHelper.GetTable<User>();
            }
        }

        /// <summary>
        /// Gets the table of 'UserRole'
        /// </summary>
        private static IList<UserRole> UserRolesTable
        {
            get
            {
                return InMemoryRepositoryHelper.GetTable<UserRole>();
            }
        }

        /// <summary>
        /// Gets the table of 'RoleFeature'.
        /// </summary>
        private static IList<RoleFeature> RoleFeaturesTable
        {
            get
            {
                return InMemoryRepositoryHelper.GetTable<RoleFeature>();
            }
        }

        /// <summary>
        /// Gets the table of 'UserTenant'
        /// </summary>
        private static IList<UserTenant> UserTenantsTable
        {
            get
            {
                return InMemoryRepositoryHelper.GetTable<UserTenant>();
            }
        }

        /// <summary>
        /// Gets the table of 'UserAccount'
        /// </summary>
        private static IList<UserAccount> UserAccountsTable
        {
            get
            {
                return InMemoryRepositoryHelper.GetTable<UserAccount>();
            }
        }

        /// <summary>
        /// Retrieves an account by its id.
        /// </summary>
        /// <param name="id">The id of the account.</param>
        /// <returns>If found, returns an instance of Account; otherwise returns null.</returns>
        public Account GetAccountById(long id)
        {
            return AccountsTable
                .Where(d => d.Id == id)
                .FirstOrDefault();
        }
        
        /// <summary>
        /// Creates or updates a 'User' in the system.
        /// </summary>
        /// <param name="entity">The entry of 'User' which will be either created or updated.</param>
        public void AddOrUpdateUser(User entity)
        {
            // validates the method input against null reference.
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            // seeks for the entry in the data source.
            // if existing, we shall update it instead
            // of creating a new one.
            var user = UsersTable
                .Where(d => d.Id == entity.Id)
                .FirstOrDefault();

            // if none, creates a new one.
            if (user == null)
            {
                user = new User();
                UsersTable.Add(user);

                // because it is an in-memory implementation
                // we generate the id based on the last entry
                var lastId = default(long);
                if (UsersTable.Count != 0)
                {
                    lastId = UsersTable.Max(d => d.Id);
                }

                user.Id = lastId + 1;
            }

            // we save the data to the data source.
            InMemoryRepositoryHelper.CopyDataAcrossEntity<User>(entity, user);

            // we retrieve the id of the returning
            // entity, based on what it was generated
            // in the data source.
            entity.Id = user.Id;
        }

        /// <summary>
        /// Gets a 'User' by its id.
        /// </summary>
        /// <param name="id">The id of the User.</param>
        /// <returns>Returns an instance of User.</returns>
        public User GetUserById(long id)
        {
            return UsersTable
                .Where(d => d.Id == id)
                .FirstOrDefault();
        }

        /// <summary>
        /// Gets a 'User' by its login.
        /// </summary>
        /// <param name="login">The login which will be matched to find the user.</param>
        /// <returns>If found, returns the instance of 'User'; otherwise, returns null.</returns>
        public User GetUserByCredentials(string login)
        {
            var user = UsersTable
                .Where(d => string.Compare(d.Credentials.Login, login, StringComparison.OrdinalIgnoreCase) == 0)
                .FirstOrDefault();

            return user;
        }

        /// <summary>
        /// Gets a 'User' by its login and password.
        /// </summary>
        /// <param name="login">The login which will be matched to find the user.</param>
        /// <param name="password">The password which will be mached to find the user.</param>
        /// <returns>If found, returns the instance of 'User'; otherwise, returns null.</returns>
        public User GetUserByCredentials(string login, string password)
        {
            var encryptedPassword = Cypher.Encrypt(password);
            
            var user = UsersTable
                .Where(d =>
                    string.Compare(d.Credentials.Login, login, false, CultureInfo.CurrentCulture) == 0 &&
                    Encoding.ASCII.GetString(d.Credentials.Password.ToArray()) == Encoding.ASCII.GetString(encryptedPassword))
                .FirstOrDefault();

            return user;
        }

        /// <summary>
        /// Gets a 'User' by its email address.
        /// </summary>
        /// <param name="emailAddress">The emailAddress which will be matched to find the user.</param>
        /// <returns>If found, returns the instance of 'User'; otherwise, returns null.</returns>
        public User GetUserByEmailAddress(string emailAddress)
        {
            var user = UsersTable
                .Where(d => string.Compare(d.EmailAddress, emailAddress, true, CultureInfo.CurrentCulture) == 0)
                .FirstOrDefault();

            return user;
        }

        /// <summary>
        /// Retrieves a list of 'User' who match to the role
        /// </summary>
        /// <param name="role">Returns a list of User.</param>
        /// <returns>Returns a list of User</returns>
        public IEnumerable<User> GetUsersByRole(AccessRoles role)
        {
            return this.GetUsersByRole(AccessLevels.None, role);
        }

        /// <summary>
        /// Retrieves a list of 'User' who match to the role
        /// </summary>
        /// <param name="level">The accesslevel for which we will try to find a matching user.</param>
        /// <param name="role">The role for which we will try to find a matching user.</param>
        /// <returns>Returns a list of User</returns>
        public IEnumerable<User> GetUsersByRole(
            AccessLevels level,
            AccessRoles role)
        {
            return UserRolesTable
                .Where(d => d.Role == role)
                .Join(
                    UsersTable,
                    memberRoles => memberRoles.UserId,
                    members => members.Id,
                    (memberRoles, members) => members)
                .Where(d =>
                    (d.AccessLevel & level) == level)
                .ToArray();
        }

        /// <summary>
        /// Retrieves a list of 'User' who are related to a tenant.
        /// </summary>
        /// <param name="tenantId">The id of the tenant.</param>
        /// <returns>Returns a list of User</returns>
        public IEnumerable<User> GetUsersByTenantId(long tenantId)
        {
            return UserTenantsTable
                .Where(d => d.TenantId == tenantId)
                .Join(
                    UsersTable,
                    userTenants => userTenants.UserId,
                    users => users.Id,
                    (userTenants, users) => users)
                .ToArray();
        }

        /// <summary>
        /// Retrieves a list of 'User' who are related to a tenant.
        /// </summary>
        /// <param name="tenantId">The id of the tenant.</param>
        /// <param name="role">The role for which we will retrieve the users</param>
        /// <returns>Returns a list of User</returns>
        public IEnumerable<User> GetUsersByTenantIdAndRole(long tenantId, AccessRoles role)
        {
            return UserRolesTable
                .Where(d => d.Role == role)
                .Join(
                    this.GetUsersByTenantId(tenantId),
                    userRoles => userRoles.UserId,
                    users => users.Id,
                    (userRoles, users) => users)
                .ToArray();
        }
        
        /// <summary>
        /// Gets the list of roles related to a particular user.
        /// </summary>
        /// <param name="login">The login of the user for which the roles will be loaded.</param>
        /// <returns>Returns a list of Role</returns>
        public IEnumerable<AccessRoles> GetRolesByCredentials(string login)
        {
            return UsersTable
                .Where(d => string.Compare(d.Credentials.Login, login, true, CultureInfo.CurrentCulture) == 0)
                .Join(
                    UserRolesTable,
                    members => members.Id,
                    memberRoles => memberRoles.UserId,
                    (members, memberRoles) => memberRoles.Role)
                .ToArray();
        }

        /// <summary>
        /// Generates an association between the user
        /// and the key of the role.
        /// </summary>
        /// <param name="userId">The id of the user.</param>
        /// <param name="role">The key of the role.</param>
        /// <returns>Returns a instance of 'UserRole' which is the association.</returns>
        public UserRole AssociateUserWithRole(long userId, AccessRoles role)
        {
            // seeks for an existing association
            // only if there's no association we
            // will attempt to create a new one.
            var userRole = UserRolesTable
                .Where(d =>
                    d.UserId == userId &&
                    d.Role == role)
                .FirstOrDefault();

            // if the association already exists
            // retrieves the user role
            if (userRole != null)
            {
                return userRole;
            }

            // otherwise we create a new association
            userRole = new UserRole()
            {
                UserId = userId,
                Role = role
            };

            // and we save it to the data source.
            UserRolesTable.Add(userRole);

            // and we return the association.
            return userRole;
        }

        /// <summary>
        /// Generates an association between the user and the tenant.
        /// </summary>
        /// <param name="userId">The id of the user.</param>
        /// <param name="tenantId">The id of the tenant.</param>
        /// <returns>Returns a instance of 'UserTenant' which is the association.</returns>
        public UserTenant AssociateUserWithTenant(long userId, long tenantId)
        {
            // seeks for an existing association
            // only if there's no association we
            // will attempt to create a new one.
            var userTenant = UserTenantsTable
                .Where(d =>
                    d.UserId == userId &&
                    d.TenantId == tenantId)
                .FirstOrDefault();

            // if the association already exists
            // retrieves the user tenant
            if (userTenant != null)
            {
                return userTenant;
            }

            // otherwise we create a new association
            userTenant = new UserTenant()
            {
                UserId = userId,
                TenantId = tenantId
            };

            // and we save it to the data source.
            UserTenantsTable.Add(userTenant);

            // and we return the association.
            return userTenant;
        }

        /// <summary>
        /// Generates an association between the role and the controlled feature.
        /// </summary>
        /// <param name="level">The access level of the role and feature.</param>
        /// <param name="role">The role which will be checked.</param>
        /// <param name="feature">The feature which is being accessed.</param>
        /// <returns>Returns a instance of 'RoleFeature' which is the association.</returns>
        public RoleFeature AssociateRoleWithFeature(
            AccessLevels level,
            AccessRoles role,
            ControlledFeatures feature)
        {
            // seeks for an existing association
            // only if there's no association we
            // will attempt to create a new one.
            var roleFeature = RoleFeaturesTable
                .Where(d =>
                    d.Level == level &&
                    d.Role == role &&
                    d.Feature == feature)
                .FirstOrDefault();

            // if the association already exists
            // retrieves the user role
            if (roleFeature != null)
            {
                return roleFeature;
            }

            // otherwise we create a new association
            roleFeature = new RoleFeature()
            {
                Level = level,
                Role = role,
                Feature = feature
            };

            // and we save it to the data source.
            RoleFeaturesTable.Add(roleFeature);

            // and we return the association.
            return roleFeature;
        }

        /// <summary>
        /// Removes an association related to level, role and feature.
        /// </summary>
        /// <param name="level">The access level of the role and feature.</param>
        /// <param name="role">The role which will be checked.</param>
        /// <param name="feature">The feature which is being accessed.</param>
        public void RemoveAssociationBetweenRoleAndFeature(
            AccessLevels level,
            AccessRoles role,
            ControlledFeatures feature)
        {
            // seeks for an existing association
            // only if there's an association we
            // will attempt to delete it.
            var roleFeature = RoleFeaturesTable
                .Where(d =>
                    d.Level == level &&
                    d.Role == role &&
                    d.Feature == feature)
                .FirstOrDefault();

            // if the association already exists
            // we kill it
            if (roleFeature != null)
            {
                RoleFeaturesTable.Remove(roleFeature);
            }
        }

        /// <summary>
        /// Retrieves a user x role association if existing.
        /// </summary>
        /// <param name="user">The user for which we are attempting to retrieve the association.</param>
        /// <param name="role">The role for which we are attempting to retrieve the association.</param>
        /// <returns>If exists, returns a UserRole association; otherwise returns false.</returns>
        public UserRole GetUserRoleAssociation(User user, AccessRoles role)
        {
            if (user == null)
            {
                throw new ArgumentNullException("user");
            }
            
            return UserRolesTable
                .Where(d =>
                    d.UserId == user.Id &&
                    d.Role == role)
                .FirstOrDefault();
        }

        /// <summary>
        /// Retrieves a AccessLevel and Role x Feature association if existing.
        /// </summary>
        /// <param name="level">The access level of the role and feature.</param>
        /// <param name="role">The role which will be checked.</param>
        /// <param name="feature">The feature which is being accessed.</param>
        /// <returns>If exists, returns a RoleFeature association; otherwise returns false.</returns>
        public RoleFeature GetRoleFeatureAssociation(
            AccessLevels level,
            AccessRoles role,
            ControlledFeatures feature)
        {
            return RoleFeaturesTable
                .Where(d =>
                    d.Level == level &&
                    d.Role == role &&
                    d.Feature == feature)
                .FirstOrDefault();
        }

        /// <summary>
        /// Gets a list of all features related to the role for a specific access level.
        /// </summary>
        /// <param name="level">The access level.</param>
        /// <returns>Returns a list of associations between role, access levels and features.</returns>
        public IEnumerable<RoleFeature> GetRoleFeaturesByAccessLevel(AccessLevels level)
        {
            return RoleFeaturesTable
                .Where(d => d.Level == level)
                .ToArray();
        }

        /// <summary>
        /// Retrives the list of User related to a particular access level.
        /// </summary>
        /// <param name="level">The access level for which the Users will be retrieved.</param>
        /// <returns>Returns an enumerable of User related to a particular access level.</returns>
        public IEnumerable<User> GetUsers(AccessLevels level)
        {
            return this.GetUsers(
                default(long),
                level);
        }

        /// <summary>
        /// Retrives the list of User related to a particular access level.
        /// </summary>
        /// <param name="visibleToUserId">The id of the user, so that we can check for related tenants or accounts, depending on the level.</param>
        /// <param name="level">The access level for which the Users will be retrieved.</param>
        /// <returns>Returns an enumerable of User related to a particular access level.</returns>
        public IEnumerable<User> GetUsers(long visibleToUserId, AccessLevels level)
        {
            if ((AccessLevels.Distributor & level) == AccessLevels.Distributor)
            {
                return UsersTable
                    .Where(d => (d.AccessLevel & level) == level)
                    .ToArray();
            }
            else if ((AccessLevels.Tenant & level) == AccessLevels.Tenant)
            {
                return GetUsersForSameTenantsOf(visibleToUserId);
            }

            return GetUsersForSameAccountsOf(visibleToUserId);
        }

        /// <summary>
        /// Retrieves users from the same tenants related
        /// to that user's tenant.
        /// </summary>
        /// <param name="userId">The id of the user who will see all other users of all tenants that he has access to.</param>
        /// <returns>Returns an enumerable of User related to the same tenants that the user identified by the userId.</returns>
        private static IEnumerable<User> GetUsersForSameTenantsOf(long userId)
        {
            var tenantIds = UserTenantsTable
                .Where(d => d.UserId == userId)
                .Select(d => d.TenantId)
                .ToArray();

            return UserTenantsTable
                .Where(d => tenantIds.Contains(d.TenantId))
                .Join(
                    UsersTable,
                    userTenants => userTenants.UserId,
                    users => users.Id,
                    (userTenants, users) => users)
                .ToArray();
        }

        /// <summary>
        /// Retrieves users from the same tenants related
        /// to that user's tenant.
        /// </summary>
        /// <param name="userId">The id of the user who will see all other users of all tenants that he has access to.</param>
        /// <returns>Returns an enumerable of User related to the same tenants that the user identified by the userId.</returns>
        private static IEnumerable<User> GetUsersForSameAccountsOf(long userId)
        {
            var accountIds = UserAccountsTable
                .Where(d => d.UserId == userId)
                .Select(d => d.AccountId)
                .ToArray();

            return UserAccountsTable
                .Where(d => accountIds.Contains(d.AccountId))
                .Join(
                    UsersTable,
                    userAccounts => userAccounts.UserId,
                    users => users.Id,
                    (userAccounts, users) => users)
                .ToArray();
        }
    }
}


